package com.gitee.wsl.compose.ext.text

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.style.ResolvedTextDirection
import com.gitee.wsl.common.ui.ext.union
import com.gitee.wsl.compose.ui.richtext.edit.annotation.ExperimentalRichTextApi
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.math.max
import kotlin.math.min

fun TextLayoutResult.getBoundingBoxes(
    start: Int,
    end: Int
): List<Rect> {

    val boxes = mutableListOf<Rect>()

    var lastRect: Rect? = null
    var lastLine: Int? = null

    for (offset in start .. end) {

        var rect = getBoundingBox(offset)
        val line = getLineForOffset(offset)

        if (lastRect != null && lastLine == line) {
            rect = lastRect.union(rect)!!
            boxes.remove(lastRect)
        }

        if (lastRect != null && lastLine != line) {

            boxes.add(
                lastRect.copy(
                    left = size.width.toFloat()
                )
            )

            boxes.remove(lastRect)
        }

        lastLine = line
        lastRect = rect
        boxes.add(rect)
    }
    return boxes
}

/**
 * Reads bounds for multiple lines. This can be removed once an
 * [official API](https://issuetracker.google.com/u/1/issues/237289433) is released.
 *
 * When [flattenForFullParagraphs] is available, the bounds for one or multiple
 * entire paragraphs is returned instead of separate lines if [startOffset]
 * and [endOffset] represent the extreme ends of those paragraph.
 *
 * @param startOffset the start offset of the range to read bounds for.
 * @param endOffset the end offset of the range to read bounds for.
 * @param flattenForFullParagraphs whether to return bounds for entire paragraphs instead of separate lines.
 * @return the list of bounds for the given range.
 */
@ExperimentalRichTextApi
fun TextLayoutResult.getBoundingBoxes(
    startOffset: Int,
    endOffset: Int,
    flattenForFullParagraphs: Boolean = false
): List<Rect> {
    if (multiParagraph.lineCount == 0)
        return emptyList()

    var lastOffset = 0
    var lastNonEmptyLineIndex = multiParagraph.lineCount - 1

    while (lastOffset == 0 && lastNonEmptyLineIndex >= 0) {
        val lastLinePosition =
            Offset(
                x = multiParagraph.getLineRight(lastNonEmptyLineIndex),
                y = multiParagraph.getLineTop(lastNonEmptyLineIndex)
            )

        lastOffset = multiParagraph.getOffsetForPosition(lastLinePosition)
        lastNonEmptyLineIndex--
    }

    if (startOffset >= lastOffset)
        return emptyList()

    if (startOffset == endOffset)
        return emptyList()

    if (startOffset < 0 || endOffset < 0 || endOffset > layoutInput.text.length)
        return emptyList()

    val start = min(startOffset, endOffset)
    val end = min(max(start, endOffset), lastOffset)

    val startLineNum = getLineForOffset(min(start, end))
    val endLineNum = getLineForOffset(max(start, end))

    if (flattenForFullParagraphs) {
        val isFullParagraph = (startLineNum != endLineNum)
                && getLineStart(startLineNum) == start
                && multiParagraph.getLineEnd(endLineNum, visibleEnd = true) == end

        if (isFullParagraph) {
            return listOf(
                Rect(
                    top = getLineTop(startLineNum),
                    bottom = getLineBottom(endLineNum),
                    left = 0f,
                    right = size.width.toFloat()
                )
            )
        }
    }

    // Compose UI does not offer any API for reading paragraph direction for an entire line.
    // So this code assumes that all paragraphs in the text will have the same direction.
    // It also assumes that this paragraph does not contain bi-directional text.
    val isLtr = multiParagraph.getParagraphDirection(offset = start) == ResolvedTextDirection.Ltr

    return fastMapRange(startLineNum, endLineNum) { lineNum ->
        val left =
            if (lineNum == startLineNum)
                getHorizontalPosition(
                    offset = start,
                    usePrimaryDirection = isLtr
                )
            else
                getLineLeft(
                    lineIndex = lineNum
                )

        val right =
            if (lineNum == endLineNum)
                getHorizontalPosition(
                    offset = end,
                    usePrimaryDirection = isLtr
                )
            else
                getLineRight(
                    lineIndex = lineNum
                )

        Rect(
            top = getLineTop(lineNum),
            bottom = getLineBottom(lineNum),
            left = left,
            right = right,
        )
    }
}


@OptIn(ExperimentalContracts::class)
internal inline fun <R> fastMapRange(
    start: Int,
    end: Int,
    transform: (Int) -> R
): List<R> {
    contract { callsInPlace(transform) }
    val destination = ArrayList<R>(/* initialCapacity = */ end - start + 1)
    for (i in start..end) {
        destination.add(transform(i))
    }
    return destination
}

/**
 * Returns the line number on which the specified text offset appears within
 * a TextLayoutResult, considering the visibility constraints imposed by maxLines.
 *
 * @param offset a character offset
 * @param checkIfDelimited indicates whether to check if the offset is within a line delimiter.
 * @return the line number associated with the specified offset, adjusted for maxLines constraints.
 *         If checkIfDelimited is true and the offset is not within a line delimiter, -1 is returned.
 */
fun TextLayoutResult.getLineForOffsetInBounds(
    offset: Int,
    checkIfDelimited: Boolean = false
): Int {
    return try {
        min(getLineForOffset(offset), lineCount - 1)
    } catch (ignore: Exception) {
        if (checkIfDelimited) {
            -1
        } else {
            lineCount - 1
        }
    }
}
